/*
 * Copyright 2009 XueSong Guo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cn.webwheel.el;

import cn.webwheel.compiler.JavaC;

import java.lang.reflect.*;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

final public class GenericClass {

    private GenericClass componentType;

    private Type type;
    private Class<?> cls;
    private Map<TypeVariable, GenericClass> tvMap = new LinkedHashMap<TypeVariable, GenericClass>();

    public GenericClass(Type type) {
        this.type = type;
        if (type instanceof Class) {
            cls = (Class) type;
        } else if (type instanceof TypeVariable) {
            TypeVariable tv = (TypeVariable) type;
            cls = new GenericClass(tv.getBounds()[0]).cls;
        } else if (type instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) type;
            cls = (Class) pt.getRawType();
            TypeVariable[] tvs = cls.getTypeParameters();
            Type[] ats = pt.getActualTypeArguments();
            for (int i = 0; i < tvs.length; i++) {
                tvMap.put(tvs[i], new GenericClass(ats[i]));
            }
        } else if (type instanceof WildcardType) {
            Type[] bs = ((WildcardType) type).getUpperBounds();
            if (bs.length > 0) {
                cls = new GenericClass(bs[0]).cls;
            }
        } else if (type instanceof GenericArrayType) {
            GenericArrayType gat = (GenericArrayType) type;
            GenericClass gc = new GenericClass(gat.getGenericComponentType());
            cls = Array.newInstance(gc.toClass(), 0).getClass();
        }
        if (type == null) return;
        if (cls == null) {
            cls = Object.class;
        } else {
            processIterator();
        }
    }

    private void processIterator() {
        if (cls == null) return;
        if (cls.isArray()) {
            if (type instanceof Class) {
                componentType = new GenericClass(cls.getComponentType());
            } else if (type instanceof GenericArrayType) {
                componentType = infer(((GenericArrayType) type).getGenericComponentType());
            }
        } else if (Iterator.class.isAssignableFrom(cls)) {
            try {
                componentType = infer(cls.getMethod("next").getGenericReturnType());
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("impossible");
            }
        } else if (Iterable.class.isAssignableFrom(cls)) {
            try {
                GenericClass gc = infer(cls.getMethod("iterator").getGenericReturnType());
                componentType = gc.infer(gc.cls.getMethod("next").getGenericReturnType());
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("impossible");
            }
        }
    }

    public GenericClass infer(Type type) {
        GenericClass gc = null;
        if (type instanceof TypeVariable) {
            TypeVariable tv = (TypeVariable) type;
            if (tv.getGenericDeclaration() instanceof Class) {
                gc = findSuperGd(this, tv);
            }
        } else if (type instanceof ParameterizedType) {
            gc = new GenericClass(type);
            for (Map.Entry<TypeVariable, GenericClass> entry : gc.tvMap.entrySet()) {
                entry.setValue(infer(entry.getValue().type));
            }
        }
        if (gc == null) gc = new GenericClass(type);
        else gc.processIterator();
        return gc;
    }

    private GenericClass findSuperGd(final GenericClass gc, final TypeVariable tv) {
        if (gc.cls == tv.getGenericDeclaration()) {
            for (Map.Entry<TypeVariable, GenericClass> entry : gc.tvMap.entrySet()) {
                if (entry.getKey() == tv) {
                    return entry.getValue();
                }
            }
            return null;
        }
        Type gsc = gc.cls.getGenericSuperclass();
        if (gsc == null) gsc = gc.cls.getSuperclass();
        if (gsc != null) {
            GenericClass gs = new GenericClass(gsc);
            gc.fill(gs);
            if (gs.cls.getTypeParameters().length > 0) {
                GenericClass ret = gc.findSuperGd(gs, tv);
                if (ret != null) return ret;
            }
        }

        for (Type t : gc.cls.getGenericInterfaces()) {
            GenericClass gs = new GenericClass(t);
            gc.fill(gs);
            if (gs.cls.getTypeParameters().length == 0) continue;
            GenericClass ret = gc.findSuperGd(gs, tv);
            if (ret != null) return ret;
        }
        return null;
    }

    private void fill(GenericClass gc) {
        for (Map.Entry<TypeVariable, GenericClass> entry : gc.tvMap.entrySet()) {
            if (entry.getValue().tvMap.isEmpty()) {
                GenericClass g = seek(tvMap, entry.getValue().type);
                if (g != null) entry.setValue(g);
            } else {
                fill(entry.getValue());
            }
        }
    }

    private GenericClass seek(Map<TypeVariable, GenericClass> tvMap, Type type) {
        for (Map.Entry<TypeVariable, GenericClass> entry : tvMap.entrySet()) {
            if (entry.getKey() == type) return entry.getValue();
            GenericClass gc = seek(entry.getValue().tvMap, type);
            if (gc != null) return gc;
        }
        return null;
    }

    public String toString() {
        if (cls == null) return "null";
        if (JavaC.javassist) {
            return cls.getCanonicalName();
        }
        if (type instanceof WildcardType) {
            WildcardType wt = (WildcardType) type;
            if (wt.getUpperBounds().length > 0) {
                return "? extends " + infer(wt.getUpperBounds()[0]).toString();
            }
        }
        if (tvMap.isEmpty()) return cls.getCanonicalName();
        StringBuilder sb = new StringBuilder(cls.getCanonicalName());
        sb.append("<");
        for (Map.Entry<TypeVariable, GenericClass> entry : tvMap.entrySet()) {
            sb.append(entry.getValue()).append(',');
        }
        sb.setCharAt(sb.length() - 1, '>');
        return sb.toString();
    }

    public String getDefaultValue() {
        if (cls == null || !cls.isPrimitive()) return "null";
        if (cls == Boolean.TYPE) {
            return "false";
        } else {
            return "0";
        }
    }

    public boolean isCollection() {
        return componentType != null;
    }

    public boolean isPrimitive() {
        return cls != null && cls.isPrimitive();
    }

    public GenericClass getComponentType() {
        return componentType;
    }

    public Class<?> toClass() {
        return cls;
    }

    public boolean isAssignableFrom(GenericClass type) {
        if (cls == null) return false;
        if (type.cls == null) return !cls.isPrimitive();
        if (!isPrimitive() && !type.isPrimitive()) {
            return cls.isAssignableFrom(type.cls);
        }
        if (type.isPrimitive() && !isPrimitive()) {
            return isAssignableFrom(wrap(type));
        }
        GenericClass t1 = toPrimitive(this);
        GenericClass t2 = toPrimitive(type);
        if (!t1.isPrimitive() || !t2.isPrimitive()) return false;
        if (t1.cls == void.class || t2.cls == void.class) return false;
        if (t1.cls == boolean.class) return t2.cls == boolean.class;
        Class[] types = new Class[]{
                Double.TYPE, Float.TYPE, Long.TYPE, Integer.TYPE, Short.TYPE, Byte.TYPE
        };
        for (int i = 0; i < types.length; i++) {
            if (types[i] == t1.cls) {
                for (int j = i; j < types.length; j++) {
                    if (types[j] == t2.cls) return true;
                }
            }
        }
        return false;
    }

    public static boolean canEquals(GenericClass type, GenericClass type2) {
        if (canCompare(type, type2)) return true;
        if (type.cls == null) return type2.cls == null || !type2.isPrimitive();
        if (type2.cls == null) return !type.isPrimitive();
        return type.isAssignableFrom(type2) || type2.isAssignableFrom(type);
    }

    public static GenericClass toPrimitive(GenericClass type) {
        if (type.cls == Boolean.class) return new GenericClass(boolean.class);
        if (type.cls == Byte.class) return new GenericClass(byte.class);
        if (type.cls == Character.class) return new GenericClass(char.class);
        if (type.cls == Short.class) return new GenericClass(short.class);
        if (type.cls == Integer.class) return new GenericClass(int.class);
        if (type.cls == Long.class) return new GenericClass(long.class);
        if (type.cls == Float.class) return new GenericClass(float.class);
        if (type.cls == Double.class) return new GenericClass(double.class);
        return type;
    }

    public static GenericClass wrap(GenericClass type) {
        if (!type.isPrimitive()) return type;
        if (type.cls == boolean.class) return new GenericClass(Boolean.class);
        if (type.cls == byte.class) return new GenericClass(Byte.class);
        if (type.cls == char.class) return new GenericClass(Character.class);
        if (type.cls == short.class) return new GenericClass(Short.class);
        if (type.cls == int.class) return new GenericClass(Integer.class);
        if (type.cls == long.class) return new GenericClass(Long.class);
        if (type.cls == float.class) return new GenericClass(Float.class);
        if (type.cls == double.class) return new GenericClass(Double.class);
        throw new RuntimeException("impossible");
    }

    public static boolean canCompare(GenericClass type, GenericClass type2) {
        if (type.cls == null || type2.cls == null) return false;
        type = toPrimitive(type);
        type2 = toPrimitive(type2);
        if (!type.cls.isPrimitive() || !type2.cls.isPrimitive()) return false;
        if (type.cls == Boolean.TYPE || type2.cls == Boolean.TYPE) return false;
        return true;
    }

    public static GenericClass canSub(GenericClass type, GenericClass type2) {
        if (type.cls == null || type2.cls == null) return null;
        type = toPrimitive(type);
        type2 = toPrimitive(type2);
        Class s = type.cls;
        Class d = type2.cls;
        if (!s.isPrimitive() || !d.isPrimitive()) return null;
        if (s == d) return type;
        if (s == Double.TYPE || d == Double.TYPE) return new GenericClass(Double.TYPE);
        if (s == Float.TYPE || d == Float.TYPE) return new GenericClass(Float.TYPE);
        if (s == Long.TYPE || d == Long.TYPE) return new GenericClass(Long.TYPE);
        if (s == Integer.TYPE || d == Integer.TYPE) return new GenericClass(Integer.TYPE);
        if (s == Short.TYPE || d == Short.TYPE) return new GenericClass(Short.TYPE);
        return null;
    }

    public static GenericClass canAdd(GenericClass type, GenericClass type2) {
        GenericClass t = canSub(type, type2);
        if (t != null) return t;
        if (type.cls == String.class) {
            return type;
        }
        if (type2.cls == String.class) {
            return type2;
        }
        return null;
    }

    public static String toPrimitive(Class type, String s) {
        if (type == Boolean.class) {
            return s + ".booleanValue()";
        } else if (type == Byte.class) {
            return s + ".byteValue()";
        } else if (type == Character.class) {
            return s + ".charValue()";
        } else if (type == Short.class) {
            return s + ".shortValue()";
        } else if (type == Integer.class) {
            return s + ".intValue()";
        } else if (type == Long.class) {
            return s + ".longValue()";
        } else if (type == Float.class) {
            return s + ".floatValue()";
        } else if (type == Double.class) {
            return s + ".doubleValue()";
        }
        return null;
    }
}
